package com.liyunc.demo.bundle.file.manager.service;

import com.google.common.collect.Sets;
import com.liyunc.demo.bundle.file.manager.model.dto.input.AssetPackageInfoRequestDto;
import com.liyunc.demo.bundle.file.manager.model.dto.input.AssetPackageRequestDto;
import com.liyunc.demo.bundle.file.manager.model.dto.output.AssetPackageDto;
import com.liyunc.demo.bundle.file.manager.utils.converter.AssetPackageConverter;
import com.liyunc.demo.bundle.file.manager.model.entity.AssetPackage;
import com.liyunc.demo.bundle.file.manager.model.entity.inline.AssetFileNeedPackInfo;
import com.liyunc.demo.bundle.file.manager.dao.repository.AssetFileEntityRepository;
import com.liyunc.demo.bundle.file.manager.dao.repository.AssetPackageEntityRepository;
import com.liyunc.demo.comp.common.db.TransactionHook;
import com.liyunc.demo.comp.common.utils.FilesUtils;
import com.liyunc.demo.comp.id.generate.service.SingIdGenerator;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;

/**
 * @author liyuncong
 * @version 1.0
 * @file AssetFilePackServiceImpl
 * @brief AssetFilePackServiceImpl
 * @details AssetFilePackServiceImpl
 * @date 2022-04-15
 *
 * Edit History
 * ----------------------------------------------------------------------------
 * DATE                        NAME               DESCRIPTION
 * 2022-04-15                  liyuncong          Created
 */
@Slf4j
public class AssetPackageService extends BaseService {

    private static final String COMPRESS_PATH = "auto_by_pack";
    private static final String COMPRESS_SUFFIX = ".tar.gz";

    private static final int PACKING_STATUS_GOING = 0;
    private static final int PACKING_STATUS_SUCCEED = 1;
    private static final int PACKING_STATUS_FAILED = 2;

    @Autowired
    private SingIdGenerator singIdGenerator;
    @Autowired
    private TransactionHook transactionHook;
    @Autowired
    private ExecutorService executorService;
    @Autowired
    private AssetFileEntityRepository fileEntityRepository;
    @Autowired
    private AssetPackageEntityRepository packageEntityRepository;
    @Autowired
    private AssetPackageConverter mapper;


    public Optional<String> packAndCompress(AssetPackageRequestDto requestDto) {
        if (Objects.isNull(requestDto)) {
            return Optional.empty();
        }
        final List<AssetPackageInfoRequestDto> list = requestDto.getFiles();
        final Long ownerId = requestDto.getOwnerId();
        final int ownerType = requestDto.getOwnerType();
        AssetPackage entity = new AssetPackage();
        entity.setId(singIdGenerator.snowFlake());
        entity.setOwnerId(ownerId);
        entity.setOwnerType(ownerType);
        //1、根据列表，检查文件数量、状态，获取列表里最新文件的时间
        Pair<Boolean, Pair<Map<Long, String>, LocalDateTime>> result =
            checkFilesAndGetLatestTime(list);
        if (!result.getLeft()) {
            log.error("Failed to check files,some file not exists");
            return Optional.empty();
        }
        LocalDateTime latestFileUpdateTimeInPackList = result.getRight().getRight();
        //2、根据请求的hash值查询是否有压缩包可以复用
        String requestBody = StringUtils.join(
            list
                .stream()
                .map(item -> item.getId() + item.getFilepath())
                .sorted()
                .toList(), ",");
        String requestHash = FilesUtils.getHash(requestBody);
        AssetPackage reusableEntity =
            packageEntityRepository.findFirstByRequestHashOrderByUpdatedAtDesc(requestHash);
        if (Objects.nonNull(reusableEntity)
            && reusableEntity.getPackStatus() == PACKING_STATUS_SUCCEED) {
            //3、比较查询到可复用的压缩包是否能直接复用，还是需要重新打包
            LocalDateTime reusableTarUpdateTime = reusableEntity.getLastTimestamp();
            // 可复用压缩包打包时间晚于或等与最新文件的更新时间，则复用
            if (reusableTarUpdateTime.isAfter(latestFileUpdateTimeInPackList)
                || reusableTarUpdateTime.isEqual(latestFileUpdateTimeInPackList)) {
                return Optional.of(reusableEntity.getId().toString());
            }
        }
        //4、根据文件列表打包压缩
        Path tarPath = Paths.get(
            COMPRESS_PATH,
            UUID.randomUUID().toString());
        Map<Long, String> needPackFilesMap = result.getRight().getLeft();
        if (Objects.isNull(needPackFilesMap)) {
            return Optional.empty();
        }
        transactionHook.afterTransaction(() ->
            handlePacking(list, tarPath, requestHash, needPackFilesMap, entity)
        );
        //5、保存压缩包信息到数据库
        entity.setRequestHash(requestHash);
        entity.setFilepath(tarPath + COMPRESS_SUFFIX);
        entity.setLastTimestamp(latestFileUpdateTimeInPackList);
        entity.setPackStatus(PACKING_STATUS_GOING);
        entity.setChecksum("");
        return Optional.empty();
    }


    public Optional<Pair<File, AssetPackageDto>> download(Long tarId) {
        final Optional<AssetPackage> result = packageEntityRepository.findById(tarId);
        if (result.isEmpty()) {
            return Optional.empty();
        }
        if (result.get().getPackStatus() == PACKING_STATUS_SUCCEED) {
            Path path = getAbsolutePath(Paths.get(result.get().getFilepath()));
            if (Files.exists(path)) {
                return Optional.of(
                    new MutablePair<>(path.toFile(), mapper.fromEntity(result.get()))
                );
            }
        }
        return Optional.empty();
    }

    /**
     * 检查文件数量，获取所有最新文件时间.
     *
     * @param list 文件列表
     * @return 文件是否匹配，最新时间
     */
    private Pair<Boolean, Pair<Map<Long, String>, LocalDateTime>> checkFilesAndGetLatestTime(
        List<AssetPackageInfoRequestDto> list) {
        if (list.isEmpty()) {
            return new MutablePair<>(Boolean.FALSE, null);
        }
        // 获取文件列表的所有id
        Set<Long> ids = list
            .stream()
            .map(AssetPackageInfoRequestDto::getId)
            .collect(Collectors.toSet());
        LocalDateTime latestFileUpdateTime;
        Map<Long, String> needPackFilesMap;
        List<AssetFileNeedPackInfo> fileList = fileEntityRepository
            .findFiles(ids);
        if (Objects.isNull(fileList) || fileList.size() != ids.size()) {
            Set<Long> missingFile = Sets.difference(
                ids,
                fileList
                    .stream()
                    .map(AssetFileNeedPackInfo::getId)
                    .collect(Collectors.toSet()));
            log.error("Failed to check packing file,some file is missing: {}",
                missingFile);
            return new MutablePair<>(Boolean.FALSE, null);
        }
        needPackFilesMap = fileList
            .stream()
            .collect(
                Collectors.toMap(AssetFileNeedPackInfo::getId, AssetFileNeedPackInfo::getPath));
        latestFileUpdateTime = fileList.get(0).getUpdatedAt();
        return new MutablePair<>(ids.size() == needPackFilesMap.size(),
            new MutablePair<>(needPackFilesMap, latestFileUpdateTime));
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void handlePacking(
        List<AssetPackageInfoRequestDto> list,
        Path tarPath,
        String requestHash,
        Map<Long, String> needPackFilesMap,
        AssetPackage entity
    ) {
        executorService.submit(() -> {
            Optional<AssetPackage> refreshOptional =
                packageEntityRepository.findById(entity.getId());
            if (refreshOptional.isEmpty()) {
                log.error("Could not find entity {}", entity.getId());
                return;
            }
            AssetPackage refresh = refreshOptional.get();
            log.debug("Started to packing... "
                    + "fileAmount={}, requestHash={}, TarPath={}",
                list.size(), tarPath, requestHash);
            boolean packed = packingAndCompressing(list, tarPath, needPackFilesMap);
            log.debug("Finished to packing... "
                    + "fileAmount={}, requestHash={}, TarPath={}",
                list.size(), tarPath, requestHash);
            Path absolutePath = getAbsolutePath(
                Paths.get(tarPath + COMPRESS_SUFFIX));
            if (!packed && Files.exists(absolutePath)) {
                //打包失败，删除压缩包，数据库记录错误打包信息
                try {
                    log.error("Packing failed need delete wrong tar");
                    Files.delete(absolutePath);
                    refresh.setPackStatus(PACKING_STATUS_FAILED);
                } catch (IOException exception) {
                    log.error("Failed to delete wrong tar {}",
                        absolutePath, exception);
                }
            } else {
                // 更新数据库打包状态
                refresh.setChecksum(FilesUtils.getChecksum(absolutePath));
                refresh.setPackStatus(PACKING_STATUS_SUCCEED);
            }
            packageEntityRepository.save(refresh);
        });
    }

    /**
     * 打包文件目录结构.
     *
     * @param list 文件列表
     */
    private boolean packingAndCompressing(
        List<AssetPackageInfoRequestDto> list,
        Path targetPath,
        Map<Long, String> needPackFilesMap) {
        AtomicBoolean packed = new AtomicBoolean(true);
        if (list.isEmpty()) {
            return false;
        }
        Path tarPath = getAbsolutePath(
            Paths.get(targetPath + COMPRESS_SUFFIX));
        if (FilesUtils.checkPath(tarPath, false)) {
            return false;
        }
        try (OutputStream output = Files.newOutputStream(tarPath);
             BufferedOutputStream bufOutput =
                 new BufferedOutputStream(output);
             GzipCompressorOutputStream zipOutput =
                 new GzipCompressorOutputStream(bufOutput);
             TarArchiveOutputStream tarOutput =
                 new TarArchiveOutputStream(zipOutput)) {
            for (var item : list) {
                Path from = getAbsolutePath(
                    Paths.get(needPackFilesMap.get(item.getId())));
                Path to = Paths.get(
                    targetPath.toString(),
                    item.getFilepath()
                );
                try {
                    TarArchiveEntry tarArchiveEntry =
                        new TarArchiveEntry(from.toFile(), to.toString());
                    tarOutput.putArchiveEntry(tarArchiveEntry);
                    Files.copy(from, tarOutput);
                    tarOutput.closeArchiveEntry();
                } catch (IOException exception) {
                    packed.set(false);
                    log.error("Failed to pack and compress file from {} to {}",
                        from, to, exception);
                    break;
                }
            }
            tarOutput.finish();
        } catch (IOException exception) {
            packed.set(false);
            log.error("Failed to pack and compress", exception);
            return packed.get();
        }
        return packed.get();
    }
}
